﻿using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;

namespace PivotIO.XML
{
    /// <summary>
    /// Some helper methods for dealing with per-collection data
    /// (like the image list or the facet list).
    /// </summary>
    public class PerCollectionDataIO<T>
        where T : new()
    {
        /// <summary>
        /// Cache the local list of the data we know about.
        /// </summary>
        protected static ImmutableDictionary<int, T[]> _cache = ImmutableDictionary.Create<int, T[]>();

        /// <summary>
        /// Filename that we should be using.
        /// </summary>
        protected static string IOName { get; set; }

        /// <summary>
        /// Reset the cache. Used for testing.
        /// </summary>
        public static void ResetCache()
        {
            _cache = ImmutableDictionary.Create<int, T[]>();
        }

        /// <summary>
        /// Load the XML DB for images given a collection id from the default location.
        /// </summary>
        /// <param name="collectionId"></param>
        /// <returns></returns>
        protected static T[] LoadAll(int collectionId)
        {
            // Check the cache

            if (_cache.ContainsKey(collectionId))
                return _cache[collectionId];

            // Load it up

            var dir = new DirectoryInfo(Path.Combine(Path.Combine(Utils.GetROOTDataDirectory().FullName, collectionId.ToString())));
            var l = LoadAll(dir);
            _cache = _cache.Add(collectionId, l);
            return l;
        }

        /// <summary>
        /// Load the location from a single directory
        /// </summary>
        /// <param name="dir"></param>
        /// <returns></returns>
        protected static T[] LoadAll(DirectoryInfo dir)
        {
            var fi = new FileInfo(Path.Combine(dir.FullName, IOName));
            if (!fi.Exists)
                return new T[0];

            // Change the text into something useful.

            var input = new BinaryFormatter();
            for (int trys = 200; trys >= 0; trys--)
            {
                try
                {
                    using (var fs = fi.OpenRead())
                    {
                        var values = input.Deserialize(fs) as T[];
                        return values;
                    }
                }
                catch
                {
                    if (trys == 0)
                        throw;
                }
                Thread.Sleep(20);
            }
            throw new FileNotFoundException(string.Format("Unable to open the file {0}", fi.FullName));
        }

        /// <summary>
        /// Set to true when the cache is dirty.
        /// </summary>
        private static ConcurrentBag<int> _cacheImageCollectionsDirty = new ConcurrentBag<int>();

        /// <summary>
        /// Save all collections to our specific output file.
        /// </summary>
        /// <param name="images"></param>
        protected static void SaveAll(int collectionId, T[] images)
        {
            // Cache it.

            _cache = _cache.SetItem(collectionId, images);
            if (!_cacheImageCollectionsDirty.Contains(collectionId))
                _cacheImageCollectionsDirty.Add(collectionId);

            // Updating the actual data can take place in a background thread.

            ThreadPool.QueueUserWorkItem(o => BackgroundSaveAll());
        }

        private static Mutex _oneSaveAtATime = new Mutex();
        private static bool _saveInProgress = false;

        /// <summary>
        /// Save everythign in the background.
        /// </summary>
        /// <returns></returns>
        private static void BackgroundSaveAll()
        {
            //
            // Only one thread may be in progress at any one time.
            //

            if (_saveInProgress)
                return;
            lock (_oneSaveAtATime)
            {
                if (_saveInProgress)
                    return;
                _saveInProgress = true;
            }

            try
            {
                //
                // Loop until we manage to run out of things to write.
                //

                while (!_cacheImageCollectionsDirty.IsEmpty)
                {
                    int collectionID;
                    if (_cacheImageCollectionsDirty.TryTake(out collectionID))
                    {
                        try
                        {
                            BackgroundSaveAllImages(collectionID, _cache[collectionID]);
                        }
                        catch { }
                    }
                }
            }
            finally
            {
                lock (_oneSaveAtATime)
                    _saveInProgress = false;
            }
        }

        /// <summary>
        /// Wait for all IO to be flusehd.
        /// </summary>
        public static void WaitTillIOFlushed()
        {
            while (true)
            {
                if (_saveInProgress)
                {
                    Thread.Sleep(10);
                }
                else
                {
                    lock (_oneSaveAtATime)
                    {
                        if (!_saveInProgress && _cacheImageCollectionsDirty.IsEmpty)
                            return;
                    }
                }
            }
        }

        /// <summary>
        /// Save a single image colletion. NOTE!!! This guy must be called while locked!
        /// </summary>
        /// <param name="item"></param>
        private static void BackgroundSaveAllImages(int collectionId, T[] images)
        {

            // Now, we need to update the actual data

            var fi = new FileInfo(Path.Combine(Utils.GetROOTDataDirectory().FullName, collectionId.ToString(), IOName));

            if (!fi.Directory.Exists)
                fi.Directory.Create();

            var output = new BinaryFormatter();

            int tries = 30;
            while (tries > 0)
            {
                try
                {
                    using (var fs = fi.Create())
                    {
                        output.Serialize(fs, images);
                        break;
                    }
                }
                catch
                {
                    tries -= 1;
                    if (tries == 0)
                        throw;
                    Thread.Sleep(50);
                }
            }

            // Mark this guy needing an update by the rendering code. The token may be locked by someone else, so
            // try a few times.
            var token = new FileInfo(Path.Combine(Utils.GetRenderWatchDirectory().FullName, collectionId.ToString()));
            for (int i = 0; i < 10; i++)
            {
                try
                {
                    using (var f = token.OpenWrite())
                    {
                    }
                    return;
                }
                catch
                {
                    Thread.Sleep(20);
                }
            }
        }

        /// <summary>
        /// Delete the images directory - everything will be lost forever!
        /// </summary>
        /// <param name="collectionId"></param>
        public static void DeleteAll(int collectionId)
        {
            var fi = new DirectoryInfo(Path.Combine(Utils.GetROOTDataDirectory().FullName, collectionId.ToString()));
            WaitTillIOFlushed();
            if (fi.Exists)
            {
                for (int i = 0; i < 20; i++)
                {
                    try
                    {
                        fi.Delete(true);
                        break;
                    }
                    catch { }
                    Thread.Sleep(50);
                }
            }
            _cache = _cache.Remove(collectionId);
        }
    }
}
